package com.bzyd.derby.plugin;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;

/**
 * mybatis允许使用自定义插件（即拦截器）对执行过程中的某一点进行拦截，可以被拦截的方法包括：
 * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
 * ParameterHandler (getParameterObject, setParameters)
 * ResultSetHandler (handleResultSets, handleOutputParameters)
 * StatementHandler (prepare, parameterize, batch, update, query)
 * <p>
 * 分页插件一般拦截的是StatementHandler.prepare方法，这个方法用于生成statement，
 * 参数包含一个Connection对象，我们可以借助它在查询发起前先先执行计数操作并替换原有的sql语句。
 * <p>
 * Intercepts注解表示这是一个拦截器，Signature注解描述具体拦截mybatis中四大对象中的哪一个，这里面只拦截StatementHandler，
 * method指定的是拦截StatementHandler中的 Statement prepare(Connection var1, Integer var2) throws SQLException;方法
 * 另外，当有重载的方法重名时，可通过args参数进一步指定。
 */
@Slf4j
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DerbyPagePlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        //获取拦截的对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

        //待执行的SQL
        BoundSql boundSql = statementHandler.getBoundSql();

        //判断是否是查询语句
        if (StringUtils.isNotBlank(boundSql.getSql()) && boundSql.getSql().toUpperCase().trim().startsWith("SELECT")) {

            //获取参数列表
            Map<String, Object> paramsMap = (Map<String, Object>) statementHandler.getParameterHandler().getParameterObject();

            //如果查询方法中有一个参数是Page类型的，则为分页查询
            if (paramsMap != null) {
                for (String key : paramsMap.keySet()) {
                    if (paramsMap.get(key) instanceof Page) {

                        Page page = (Page) paramsMap.get(key);

                        //获取数据库连接
                        Connection connection = (Connection) invocation.getArgs()[0];

                        //MetaObject是mybatis的底层工具类，能够让我们用“属性.属性”这种方便的方式，对实例化的对象进行赋值和取值。
                        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

                        //查询总条数
                        int count = selectCount(connection, (ParameterHandler) metaObject.getValue("delegate.parameterHandler"), boundSql);
                        page.setCount(count);

                        //拼接分页SQL
                        String pageSql = PageSqlUtil.getPageSql(page, boundSql.getSql());
                        metaObject.setValue("delegate.boundSql.sql", pageSql);
                        break;
                    }
                }
            }
        }

        return invocation.proceed();//执行查询
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 查询总条数
     *
     * @param connection
     * @param parameterHandler
     * @param boundSql
     * @return
     */
    private int selectCount(Connection connection, ParameterHandler parameterHandler, BoundSql boundSql) throws SQLException {
        String countSql = PageSqlUtil.getCountSql(boundSql.getSql());
        PreparedStatement countStmt = null;
        ResultSet rs = null;

        try {
            countStmt = connection.prepareStatement(countSql);
            parameterHandler.setParameters(countStmt);
            rs = countStmt.executeQuery();
            if (rs.next()) {
                return rs.getInt(1);
            }
        } finally {
            if (countStmt != null) {
                try {
                    countStmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    countStmt = null;
                }
            }

            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    rs = null;
                }
            }
        }
        return 0;
    }
}
